/*
 * Copyright 2015 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.inferred.freebuilder.processor.property;

import static org.inferred.freebuilder.processor.GeneratedTypeSubject.assertThat;
import static org.inferred.freebuilder.processor.model.ClassTypeImpl.INTEGER;
import static org.inferred.freebuilder.processor.model.ClassTypeImpl.STRING;
import static org.inferred.freebuilder.processor.model.GenericTypeElementImpl.newTopLevelGenericType;
import static org.inferred.freebuilder.processor.model.PrimitiveTypeImpl.INT;
import static org.inferred.freebuilder.processor.model.WildcardTypeImpl.wildcardSuper;
import static org.inferred.freebuilder.processor.source.FunctionalType.consumer;

import com.google.common.collect.ImmutableMap;

import org.inferred.freebuilder.processor.BuilderFactory;
import org.inferred.freebuilder.processor.Datatype;
import org.inferred.freebuilder.processor.GeneratedBuilder;
import org.inferred.freebuilder.processor.model.GenericTypeElementImpl;
import org.inferred.freebuilder.processor.model.GenericTypeElementImpl.GenericTypeMirrorImpl;
import org.inferred.freebuilder.processor.source.QualifiedName;
import org.inferred.freebuilder.processor.source.feature.GuavaLibrary;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Optional;

@RunWith(JUnit4.class)
public class MapSourceTest {

  @Test
  public void test_noGuava() {
    assertThat(builder()).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Person;",
        "import java.util.Collection;",
        "import java.util.Collections;",
        "import java.util.LinkedHashMap;",
        "import java.util.Map;",
        "import java.util.Objects;",
        "import java.util.function.Consumer;",
        "",
        "/** Auto-generated superclass of {@link Person.Builder}, "
            + "derived from the API of {@link Person}. */",
        "abstract class Person_Builder {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static Person.Builder from(Person value) {",
        "    if (value instanceof Rebuildable) {",
        "      return ((Rebuildable) value).toBuilder();",
        "    } else {",
        "      return new Person.Builder().mergeFrom(value);",
        "    }",
        "  }",
        "",
        "  private final LinkedHashMap<Integer, String> name = new LinkedHashMap<>();",
        "",
        "  /**",
        "   * Associates {@code key} with {@code value} in the map to be returned from "
            + "{@link Person#name()}.",
        "   * If the map previously contained a mapping for the key, "
            + "the old value is replaced by the",
        "   * specified value.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code value} is null",
        "   */",
        "  public Person.Builder putName(int key, String value) {",
        "    Objects.requireNonNull(value);",
        "    name.put(key, value);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies all of the mappings from {@code map} to the map to be returned from {@link",
        "   * Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code map} is null or contains a null key or value",
        "   */",
        "  public Person.Builder putAllName(Map<? extends Integer, ? extends String> map) {",
        "    for (Map.Entry<? extends Integer, ? extends String> entry : map.entrySet()) {",
        "      putName(entry.getKey(), entry.getValue());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Removes the mapping for {@code key} from the map to be returned from "
            + "{@link Person#name()}, if",
        "   * one is present.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder removeName(int key) {",
        "    name.remove(key);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Invokes {@code mutator} with the map to be returned from {@link Person#name()}.",
        "   *",
        "   * <p>This method mutates the map in-place. {@code mutator} is a void consumer, so any "
            + "value",
        "   * returned from a lambda will be ignored. Take care not to call pure functions, like "
            + "{@link",
        "   * Collection#stream()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mutator} is null",
        "   */",
        "  public Person.Builder mutateName(Consumer<? super Map<Integer, String>> mutator) {",
        "    // If putName is overridden, this method will be updated to delegate to it",
        "    mutator.accept(name);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Removes all of the mappings from the map to be returned from "
            + "{@link Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clearName() {",
        "    name.clear();",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Returns an unmodifiable view of the map that will be returned by "
            + "{@link Person#name()}. Changes",
        "   * to this builder will be reflected in the view.",
        "   */",
        "  public Map<Integer, String> name() {",
        "    return Collections.unmodifiableMap(name);",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}, appending to collections.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person value) {",
        "    putAllName(value.name());",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, appending to collections.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person.Builder template) {",
        "    // Upcast to access private fields; otherwise, oddly, we get an access violation.",
        "    Person_Builder base = template;",
        "    putAllName(base.name);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clear() {",
        "    name.clear();",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /** Returns a newly-created {@link Person} based on the contents of this "
            + "{@code Builder}. */",
        "  public Person build() {",
        "    return new Value(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Person} for use in unit tests. "
            + "State checking will not",
        "   * be performed.",
        "   *",
        "   * <p>The builder returned by {@link Person.Builder#from(Person)} will propagate the "
            + "partial",
        "   * status of its input, overriding {@link Person.Builder#build() build()} to return "
            + "another",
        "   * partial. This allows for robust tests of modify-rebuild code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  public Person buildPartial() {",
        "    return new Partial(this);",
        "  }",
        "",
        "  private abstract static class Rebuildable extends Person {",
        "    public abstract Person.Builder toBuilder();",
        "  }",
        "",
        "  private static final class Value extends Rebuildable {",
        "    private final Map<Integer, String> name;",
        "",
        "    private Value(Person_Builder builder) {",
        "      this.name = immutableMap(builder.name);",
        "    }",
        "",
        "    @Override",
        "    public Map<Integer, String> name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new Person.Builder();",
        "      builder.name.putAll(name);",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value other = (Value) obj;",
        "      return Objects.equals(name, other.name);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"Person{name=\" + name + \"}\";",
        "    }",
        "  }",
        "",
        "  private static final class Partial extends Rebuildable {",
        "    private final Map<Integer, String> name;",
        "",
        "    Partial(Person_Builder builder) {",
        "      this.name = immutableMap(builder.name);",
        "    }",
        "",
        "    @Override",
        "    public Map<Integer, String> name() {",
        "      return name;",
        "    }",
        "",
        "    private static class PartialBuilder extends Person.Builder {",
        "      @Override",
        "      public Person build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new PartialBuilder();",
        "      builder.name.putAll(name);",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial other = (Partial) obj;",
        "      return Objects.equals(name, other.name);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"partial Person{name=\" + name + \"}\";",
        "    }",
        "  }",
        "",
        "  private static <K, V> Map<K, V> immutableMap(Map<K, V> entries) {",
        "    switch (entries.size()) {",
        "      case 0:",
        "        return Collections.emptyMap();",
        "      case 1:",
        "        Map.Entry<K, V> entry = entries.entrySet().iterator().next();",
        "        return Collections.singletonMap(entry.getKey(), entry.getValue());",
        "      default:",
        "        return Collections.unmodifiableMap(new LinkedHashMap<>(entries));",
        "    }",
        "  }",
        "}");
  }

  @Test
  public void test_guava() {
    assertThat(builder()).given(GuavaLibrary.AVAILABLE).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Person;",
        "import com.google.common.annotations.VisibleForTesting;",
        "import com.google.common.collect.ImmutableMap;",
        "import java.util.Collection;",
        "import java.util.Collections;",
        "import java.util.LinkedHashMap;",
        "import java.util.Map;",
        "import java.util.Objects;",
        "import java.util.function.Consumer;",
        "",
        "/** Auto-generated superclass of {@link Person.Builder}, "
            + "derived from the API of {@link Person}. */",
        "abstract class Person_Builder {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static Person.Builder from(Person value) {",
        "    if (value instanceof Rebuildable) {",
        "      return ((Rebuildable) value).toBuilder();",
        "    } else {",
        "      return new Person.Builder().mergeFrom(value);",
        "    }",
        "  }",
        "",
        "  private final LinkedHashMap<Integer, String> name = new LinkedHashMap<>();",
        "",
        "  /**",
        "   * Associates {@code key} with {@code value} in the map to be returned from "
            + "{@link Person#name()}.",
        "   * If the map previously contained a mapping for the key, "
            + "the old value is replaced by the",
        "   * specified value.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code value} is null",
        "   */",
        "  public Person.Builder putName(int key, String value) {",
        "    Objects.requireNonNull(value);",
        "    name.put(key, value);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies all of the mappings from {@code map} to the map to be returned from {@link",
        "   * Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code map} is null or contains a null key or value",
        "   */",
        "  public Person.Builder putAllName(Map<? extends Integer, ? extends String> map) {",
        "    for (Map.Entry<? extends Integer, ? extends String> entry : map.entrySet()) {",
        "      putName(entry.getKey(), entry.getValue());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Removes the mapping for {@code key} from the map to be returned from "
            + "{@link Person#name()}, if",
        "   * one is present.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder removeName(int key) {",
        "    name.remove(key);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Invokes {@code mutator} with the map to be returned from {@link Person#name()}.",
        "   *",
        "   * <p>This method mutates the map in-place. {@code mutator} is a void consumer, so any "
            + "value",
        "   * returned from a lambda will be ignored. Take care not to call pure functions, like "
            + "{@link",
        "   * Collection#stream()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mutator} is null",
        "   */",
        "  public Person.Builder mutateName(Consumer<? super Map<Integer, String>> mutator) {",
        "    // If putName is overridden, this method will be updated to delegate to it",
        "    mutator.accept(name);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Removes all of the mappings from the map to be returned from {@link Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clearName() {",
        "    name.clear();",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Returns an unmodifiable view of the map that will be returned by "
            + "{@link Person#name()}. Changes",
        "   * to this builder will be reflected in the view.",
        "   */",
        "  public Map<Integer, String> name() {",
        "    return Collections.unmodifiableMap(name);",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}, appending to collections.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person value) {",
        "    putAllName(value.name());",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, appending to collections.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person.Builder template) {",
        "    // Upcast to access private fields; otherwise, oddly, we get an access violation.",
        "    Person_Builder base = template;",
        "    putAllName(base.name);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clear() {",
        "    name.clear();",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /** Returns a newly-created {@link Person} based on the contents of this "
            + "{@code Builder}. */",
        "  public Person build() {",
        "    return new Value(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Person} for use in unit tests. "
            + "State checking will not",
        "   * be performed.",
        "   *",
        "   * <p>The builder returned by {@link Person.Builder#from(Person)} will propagate the "
            + "partial",
        "   * status of its input, overriding {@link Person.Builder#build() build()} to return "
            + "another",
        "   * partial. This allows for robust tests of modify-rebuild code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  @VisibleForTesting()",
        "  public Person buildPartial() {",
        "    return new Partial(this);",
        "  }",
        "",
        "  private abstract static class Rebuildable extends Person {",
        "    public abstract Person.Builder toBuilder();",
        "  }",
        "",
        "  private static final class Value extends Rebuildable {",
        "    private final ImmutableMap<Integer, String> name;",
        "",
        "    private Value(Person_Builder builder) {",
        "      this.name = ImmutableMap.copyOf(builder.name);",
        "    }",
        "",
        "    @Override",
        "    public Map<Integer, String> name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new Person.Builder();",
        "      builder.name.putAll(name);",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value other = (Value) obj;",
        "      return Objects.equals(name, other.name);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"Person{name=\" + name + \"}\";",
        "    }",
        "  }",
        "",
        "  private static final class Partial extends Rebuildable {",
        "    private final ImmutableMap<Integer, String> name;",
        "",
        "    Partial(Person_Builder builder) {",
        "      this.name = ImmutableMap.copyOf(builder.name);",
        "    }",
        "",
        "    @Override",
        "    public Map<Integer, String> name() {",
        "      return name;",
        "    }",
        "",
        "    private static class PartialBuilder extends Person.Builder {",
        "      @Override",
        "      public Person build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new PartialBuilder();",
        "      builder.name.putAll(name);",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial other = (Partial) obj;",
        "      return Objects.equals(name, other.name);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"partial Person{name=\" + name + \"}\";",
        "    }",
        "  }",
        "}");
  }

  private static GeneratedBuilder builder() {
    GenericTypeElementImpl map = newTopLevelGenericType("java.util.Map");
    GenericTypeMirrorImpl mapIntString = map.newMirror(INTEGER, STRING);
    QualifiedName person = QualifiedName.of("com.example", "Person");
    QualifiedName generatedBuilder = QualifiedName.of("com.example", "Person_Builder");

    Datatype datatype = new Datatype.Builder()
        .setBuilder(person.nestedType("Builder").withParameters())
        .setExtensible(true)
        .setBuilderFactory(BuilderFactory.NO_ARGS_CONSTRUCTOR)
        .setBuilderSerializable(false)
        .setGeneratedBuilder(generatedBuilder.withParameters())
        .setInterfaceType(false)
        .setPartialType(generatedBuilder.nestedType("Partial").withParameters())
        .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters())
        .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters())
        .setType(person.withParameters())
        .setValueType(generatedBuilder.nestedType("Value").withParameters())
        .build();
    Property name = new Property.Builder()
        .setAllCapsName("NAME")
        .setBoxedType(mapIntString)
        .setCapitalizedName("Name")
        .setFullyCheckedCast(true)
        .setGetterName("name")
        .setName("name")
        .setType(mapIntString)
        .setUsingBeanConvention(false)
        .build();

    return new GeneratedBuilder(datatype, ImmutableMap.of(
        name, new MapProperty(
                datatype,
                name,
                false,
                INTEGER,
                Optional.of(INT),
                STRING,
                Optional.empty(),
                consumer(wildcardSuper(mapIntString)))));
  }
}
